/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule EventPluginRegistry
 */

'use strict'

var invariant = require('fbjs/lib/invariant')

/**
 * Injectable ordering of event plugins.
 */
var EventPluginOrder = null

/**
 * Injectable mapping from names to event plugin modules.
 */
var namesToPlugins = {}

/**
 * Recomputes the plugin list using the injected plugins and plugin ordering.
 *
 * @private
 */
function recomputePluginOrdering() {
  if (!EventPluginOrder) {
    // Wait until an `EventPluginOrder` is injected.
    return
  }
  for (var pluginName in namesToPlugins) {
    var PluginModule = namesToPlugins[pluginName]
    var pluginIndex = EventPluginOrder.indexOf(pluginName)
    !(pluginIndex > -1)
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'EventPluginRegistry: Cannot inject event plugins that do not exist in ' +
              'the plugin ordering, `%s`.',
            pluginName
          )
        : invariant(false)
      : void 0
    if (EventPluginRegistry.plugins[pluginIndex]) {
      continue
    }
    !PluginModule.extractEvents
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'EventPluginRegistry: Event plugins must implement an `extractEvents` ' +
              'method, but `%s` does not.',
            pluginName
          )
        : invariant(false)
      : void 0
    EventPluginRegistry.plugins[pluginIndex] = PluginModule
    var publishedEvents = PluginModule.eventTypes
    for (var eventName in publishedEvents) {
      !publishEventForPlugin(
        publishedEvents[eventName],
        PluginModule,
        eventName
      )
        ? process.env.NODE_ENV !== 'production'
          ? invariant(
              false,
              'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',
              eventName,
              pluginName
            )
          : invariant(false)
        : void 0
    }
  }
}

/**
 * Publishes an event so that it can be dispatched by the supplied plugin.
 *
 * @param {object} dispatchConfig Dispatch configuration for the event.
 * @param {object} PluginModule Plugin publishing the event.
 * @return {boolean} True if the event was successfully published.
 * @private
 */
function publishEventForPlugin(dispatchConfig, PluginModule, eventName) {
  !!EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName)
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          'EventPluginHub: More than one plugin attempted to publish the same ' +
            'event name, `%s`.',
          eventName
        )
      : invariant(false)
    : void 0
  EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig

  var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames
  if (phasedRegistrationNames) {
    for (var phaseName in phasedRegistrationNames) {
      if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
        var phasedRegistrationName = phasedRegistrationNames[phaseName]
        publishRegistrationName(phasedRegistrationName, PluginModule, eventName)
      }
    }
    return true
  } else if (dispatchConfig.registrationName) {
    publishRegistrationName(
      dispatchConfig.registrationName,
      PluginModule,
      eventName
    )
    return true
  }
  return false
}

/**
 * Publishes a registration name that is used to identify dispatched events and
 * can be used with `EventPluginHub.putListener` to register listeners.
 *
 * @param {string} registrationName Registration name to add.
 * @param {object} PluginModule Plugin publishing the event.
 * @private
 */
function publishRegistrationName(registrationName, PluginModule, eventName) {
  !!EventPluginRegistry.registrationNameModules[registrationName]
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          'EventPluginHub: More than one plugin attempted to publish the same ' +
            'registration name, `%s`.',
          registrationName
        )
      : invariant(false)
    : void 0
  EventPluginRegistry.registrationNameModules[registrationName] = PluginModule
  EventPluginRegistry.registrationNameDependencies[registrationName] =
    PluginModule.eventTypes[eventName].dependencies

  if (process.env.NODE_ENV !== 'production') {
    var lowerCasedName = registrationName.toLowerCase()
    EventPluginRegistry.possibleRegistrationNames[lowerCasedName] =
      registrationName
  }
}

/**
 * Registers plugins so that they can extract and dispatch events.
 *
 * @see {EventPluginHub}
 */
var EventPluginRegistry = {
  /**
   * Ordered list of injected plugins.
   */
  plugins: [],

  /**
   * Mapping from event name to dispatch config
   */
  eventNameDispatchConfigs: {},

  /**
   * Mapping from registration name to plugin module
   */
  registrationNameModules: {},

  /**
   * Mapping from registration name to event name
   */
  registrationNameDependencies: {},

  /**
   * Mapping from lowercase registration names to the properly cased version,
   * used to warn in the case of missing event handlers. Available
   * only in __DEV__.
   * @type {Object}
   */
  possibleRegistrationNames: process.env.NODE_ENV !== 'production' ? {} : null,

  /**
   * Injects an ordering of plugins (by plugin name). This allows the ordering
   * to be decoupled from injection of the actual plugins so that ordering is
   * always deterministic regardless of packaging, on-the-fly injection, etc.
   *
   * @param {array} InjectedEventPluginOrder
   * @internal
   * @see {EventPluginHub.injection.injectEventPluginOrder}
   */
  injectEventPluginOrder: function (InjectedEventPluginOrder) {
    !!EventPluginOrder
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'EventPluginRegistry: Cannot inject event plugin ordering more than ' +
              'once. You are likely trying to load more than one copy of React.'
          )
        : invariant(false)
      : void 0
    // Clone the ordering so it cannot be dynamically mutated.
    EventPluginOrder = Array.prototype.slice.call(InjectedEventPluginOrder)
    recomputePluginOrdering()
  },

  /**
   * Injects plugins to be used by `EventPluginHub`. The plugin names must be
   * in the ordering injected by `injectEventPluginOrder`.
   *
   * Plugins can be injected as part of page initialization or on-the-fly.
   *
   * @param {object} injectedNamesToPlugins Map from names to plugin modules.
   * @internal
   * @see {EventPluginHub.injection.injectEventPluginsByName}
   */
  injectEventPluginsByName: function (injectedNamesToPlugins) {
    var isOrderingDirty = false
    for (var pluginName in injectedNamesToPlugins) {
      if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
        continue
      }
      var PluginModule = injectedNamesToPlugins[pluginName]
      if (
        !namesToPlugins.hasOwnProperty(pluginName) ||
        namesToPlugins[pluginName] !== PluginModule
      ) {
        !!namesToPlugins[pluginName]
          ? process.env.NODE_ENV !== 'production'
            ? invariant(
                false,
                'EventPluginRegistry: Cannot inject two different event plugins ' +
                  'using the same name, `%s`.',
                pluginName
              )
            : invariant(false)
          : void 0
        namesToPlugins[pluginName] = PluginModule
        isOrderingDirty = true
      }
    }
    if (isOrderingDirty) {
      recomputePluginOrdering()
    }
  },

  /**
   * Looks up the plugin for the supplied event.
   *
   * @param {object} event A synthetic event.
   * @return {?object} The plugin that created the supplied event.
   * @internal
   */
  getPluginModuleForEvent: function (event) {
    var dispatchConfig = event.dispatchConfig
    if (dispatchConfig.registrationName) {
      return (
        EventPluginRegistry.registrationNameModules[
          dispatchConfig.registrationName
        ] || null
      )
    }
    for (var phase in dispatchConfig.phasedRegistrationNames) {
      if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
        continue
      }
      var PluginModule =
        EventPluginRegistry.registrationNameModules[
          dispatchConfig.phasedRegistrationNames[phase]
        ]
      if (PluginModule) {
        return PluginModule
      }
    }
    return null
  },

  /**
   * Exposed for unit testing.
   * @private
   */
  _resetEventPlugins: function () {
    EventPluginOrder = null
    for (var pluginName in namesToPlugins) {
      if (namesToPlugins.hasOwnProperty(pluginName)) {
        delete namesToPlugins[pluginName]
      }
    }
    EventPluginRegistry.plugins.length = 0

    var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs
    for (var eventName in eventNameDispatchConfigs) {
      if (eventNameDispatchConfigs.hasOwnProperty(eventName)) {
        delete eventNameDispatchConfigs[eventName]
      }
    }

    var registrationNameModules = EventPluginRegistry.registrationNameModules
    for (var registrationName in registrationNameModules) {
      if (registrationNameModules.hasOwnProperty(registrationName)) {
        delete registrationNameModules[registrationName]
      }
    }

    if (process.env.NODE_ENV !== 'production') {
      var possibleRegistrationNames =
        EventPluginRegistry.possibleRegistrationNames
      for (var lowerCasedName in possibleRegistrationNames) {
        if (possibleRegistrationNames.hasOwnProperty(lowerCasedName)) {
          delete possibleRegistrationNames[lowerCasedName]
        }
      }
    }
  }
}

module.exports = EventPluginRegistry
